/*
 * Copyright 2013 Solace Systems, Inc.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.solacesystems.tools.ant;

import static org.junit.Assert.*;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.ccil.cowan.tagsoup.Parser;
import org.junit.Test;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class LinkFinderTests {

	static abstract class LinkEventHandler extends LinkFinder.LinkEventListener {
		@Override
		public void error(Location location, Exception e) {
			fail("error occured");
		}
	}
		
	static final class AttributesImpl implements Attributes {
		private final Map<String, String> m_Attrs = new HashMap<String, String>();
		private final String[] m_Names;
		private final String[] m_Values;
		
		public AttributesImpl(String[] names, String[] values) {
			assert(names.length == values.length);
			m_Names = names;
			m_Values = values;
			for (int ii = 0; ii < names.length; ii++) {
				m_Attrs.put(names[ii], values[ii]);
			}
		}
		
		@Override
		public int getLength() {
			return m_Names.length;
		}

		@Override
		public String getURI(int index) {
			if (index >= m_Names.length) {
				return null; // Out of range
			} else {
				return ""; // No namespace
			}
		}

		@Override
		public String getLocalName(int index) {
			try {
				return m_Names[index];
			} catch (IndexOutOfBoundsException e) {
				return null;
			}
		}

		@Override
		public String getQName(int index) {
			// Not sure if this is correct or not, but its good enough for now
			return getLocalName(index);
		}

		@Override
		public String getType(int index) {
			return "CDATA";
		}

		@Override
		public String getValue(int index) {
			try {
				return m_Values[index];
			} catch (IndexOutOfBoundsException e) {
				return null;
			}
		}

		@Override
		public int getIndex(String uri, String localName) {
			if (uri.isEmpty()) {
				for (int ii = 0; ii < m_Names.length; ii++) {
					if (m_Names[ii].equals(localName)) {
						return ii;
					}
				}
			}
			return -1;
		}

		@Override
		public int getIndex(String qName) {
			// Not sure if correct
			return getIndex("", qName);
		}

		@Override
		public String getType(String uri, String localName) {
			if (getIndex(uri, localName) != -1) {
				return "CDATA";
			}
			return null;
		}

		@Override
		public String getType(String qName) {
			if (getIndex(qName) != -1) {
				return "CDATA";
			}
			return null;
		}

		@Override
		public String getValue(String uri, String localName) {
			if (uri.isEmpty()) {
				return m_Attrs.get(localName);
			}
			return null;
		}

		@Override
		public String getValue(String qName) {
			// Not sure if correct
			return getValue("", qName);
		}
	}
		
	@Test
	public void testGetSetLinkAttributeNames() {
		LinkFinder lf = new LinkFinder();
		ArrayList<String> names = new ArrayList<String>(Arrays.asList(lf.getLinkAttributeNames()));
		names.add("test");
		lf.setLinkAttributeNames(names.toArray(new String[names.size()]));
		
		assertArrayEquals(names.toArray(), lf.getLinkAttributeNames());
	}

	@Test
	public void testGetSetLocator() {
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		assertEquals(TestConst.GOOD_LOCATOR, lf.getCurrentLocator());
	}

	@Test
	public void testGetDocumentURIGood() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		assertEquals(TestConst.GOOD_URI_STR, lf.getDocumentURI().toString());
	}

	@Test(expected = URISyntaxException.class)
	public void testGetDocumentURIBad() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.BAD_URI_LOCATOR);
		assertEquals(TestConst.BAD_URI_STR, lf.getDocumentURI().toString()); // Should throw here
	}

	@Test
	public void testRelativeToAbsoluteSimple() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		
		URI result = lf.relativeToAbsolute("banana/test.html");
		
		assertEquals(TestConst.GOOD_URI_STR + "/banana/test.html", result.toString());
	}
	
	@Test
	public void testFixLink() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		String result = lf.fixLink(TestConst.BAD_URI_STR2);
		URI uri = new URI(result);
		assertTrue("http://example.org/badurl/?banana=hello%20world".equals(uri.toString()) ||
				"http://example.org/badurl/?banana=hello+world".equals(uri.toString()));
	}
	
	@Test
	public void testFixLink2() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		String result = lf.fixLink(TestConst.BAD_URI_STR3);
		URI uri = new URI(result);
		assertTrue("http://example.org/badurl/?banana=hello%20world".equals(uri.toString()) ||
				"http://example.org/badurl/?banana=hello+world".equals(uri.toString()));
	}

	/**
	 * Wraps an object to get around the final requirement in callbacks.
	 * 
	 * Java is not my best language, so forgive me for implementing this if
	 * there is a more elegant way of doing it.
	 * @author swilson
	 *
	 * @param <E>
	 */
	static class Box<E> {
		public E value;
		
		public Box() {}
		public Box(E value) {
			this.value = value;
		}
	}
	
	@Test
	public void testAddLink() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		final Box<Boolean> linkFound = new Box<Boolean>(false);
		
		lf.addLinkEventListener(new LinkEventHandler() {
			@Override
			public void linkFound(Location location, URI link) {
				linkFound.value = Boolean.TRUE;
			}
		});
		lf.addLink(TestConst.GOOD_URI_STR);
		assertTrue("no link found", linkFound.value.booleanValue());
	}

	private Attributes getAttributes(boolean inclBad) {
		String maybe = inclBad ? TestConst.BAD_URI_STR : TestConst.GOOD_URI_STR;
		return new AttributesImpl(
				new String[] { "href", "action", "style", "src"},
				new String[] { TestConst.GOOD_URI_STR, maybe, TestConst.GOOD_URI_STR, TestConst.GOOD_URI_STR});	
	}
	
	@Test
	public void testAddLinksFrom() {
		Attributes attrs = getAttributes(true);
		LinkFinder lf = new LinkFinder();
		final Box<Integer> count = new Box<Integer>(0);
		final Box<Integer> error = new Box<Integer>(0);
		
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		
		lf.addLinkEventListener(new LinkFinder.LinkEventListener() {
			@Override
			public void linkFound(Location location, URI link) {
				count.value ++;
			}

			@Override
			public void error(Location location, Exception e) {
				if (!(e instanceof URISyntaxException)) {
					fail("unexpected exception thrown");
				}
				error.value++;
			}
		});
		
		lf.addLinksFrom(attrs);
		assertEquals(2, count.value.intValue());
		assertEquals(1, error.value.intValue());
	}

	@Test
	public void testStartElementBase() throws SAXException, URISyntaxException {
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		AttributesImpl attrs = new AttributesImpl(
				new String[] { "href" }, 
				new String[] { TestConst.GOOD_URI_STR2 });
		
		lf.addLinkEventListener(new LinkEventHandler() {
			@Override
			public void linkFound(Location location, URI link) {
				fail("no new links should be found for base tags");
			}
		});
		
		lf.startElement("", "base", "base", attrs); // Not 100% sure what qname should be for HTML files
		
		// Test that relative URIs are resolved against the URI from the base tag
		assertEquals(TestConst.GOOD_URI_STR2, lf.relativeToAbsolute(".").toString());
	}
	
	@Test
	public void testStartElementBase2() throws SAXException, URISyntaxException {
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		AttributesImpl attrs1 = new AttributesImpl(
				new String[] { "href" }, 
				new String[] { TestConst.GOOD_URI_STR2 });
		
		AttributesImpl attrs2 = new AttributesImpl(
				new String[] { "href" }, 
				new String[] { TestConst.GOOD_URI_STR3 });
		
		lf.addLinkEventListener(new LinkEventHandler() {
			@Override
			public void linkFound(Location location, URI link) {
				fail("no new links should be found for base tags");
			}
		});
		
		lf.startElement("", "base", "base", attrs1); // Not 100% sure what qname should be for HTML files
		lf.endElement("", "base", "base");
		
		lf.startElement("", "base", "base", attrs2);
		lf.endElement("", "base", "base");
		
		// Test that relative URIs are resolved against the URI from the *first* base tag
		assertEquals(TestConst.GOOD_URI_STR2, lf.relativeToAbsolute(".").toString());
	}
	
	@Test
	public void testStartElement() throws SAXException {
		Attributes attrs = getAttributes(false);
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		
		final Box<Integer> count = new Box<Integer>(0);
		
		lf.addLinkEventListener(new LinkEventHandler() {
			@Override
			public void linkFound(Location location, URI link) {
				count.value++;
			}
		});
		
		lf.startElement("", "a", "a", attrs);
		
		assertEquals(3, count.value.intValue());
	}

	@Test
	public void testReset() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		lf.setDocumentLocator(TestConst.BAD_URI_LOCATOR);
		lf.reset();
		
		lf.setDocumentLocator(TestConst.GOOD_LOCATOR);
		assertEquals(TestConst.GOOD_URI_STR, lf.getDocumentURI().toString());
	}

	@Test
	public void testTriggerLinkFound() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		final Box<Boolean> triggered = new Box<Boolean>(false);
		lf.addLinkEventListener(new LinkEventHandler() {
			@Override
			public void linkFound(Location location, URI link) {
				triggered.value = true;
			}
		});
		
		lf.triggerLinkFound(TestConst.GOOD_LOCATOR, new URI(TestConst.GOOD_URI_STR2));
		
		assertTrue(triggered.value);
	}

	@Test
	public void testTriggerError() {
		LinkFinder lf = new LinkFinder();
		final Box<Boolean> triggered = new Box<Boolean>(false);
		lf.addLinkEventListener(new LinkFinder.LinkEventListener() {
			@Override
			public void linkFound(Location location, URI link) {
				fail("no link should have been found");
			}

			@Override
			public void error(Location location, Exception e) {
				triggered.value = true;
			}
		});
		
		lf.triggerError(TestConst.GOOD_LOCATOR, new Exception());
		
		assertTrue(triggered.value);
	}

	@Test
	public void testAddRemoveLinkEventListener() throws URISyntaxException {
		LinkFinder lf = new LinkFinder();
		final Box<Boolean> triggered = new Box<Boolean>(false);
		LinkEventHandler handler = new LinkEventHandler() {
			@Override
			public void linkFound(Location location, URI link) {
				triggered.value = true;
			}
		};
	
		// Test Adding a link event listener
		lf.addLinkEventListener(handler);
		lf.triggerLinkFound(TestConst.GOOD_LOCATOR, new URI(TestConst.GOOD_URI_STR2));
		
		assertTrue(triggered.value.booleanValue());
		
		// Reset the test
		triggered.value = false;
		
		// Test removing the same listener
		lf.removeLinkEventListener(handler);
		lf.triggerLinkFound(TestConst.GOOD_LOCATOR, new URI(TestConst.GOOD_URI_STR2));
		
		assertFalse(triggered.value.booleanValue());
	}
	
	@Test
	public void testFull() throws IOException, SAXException {
		final String document = "<!DOCTYPE html>"							   +
								"<html>"									   +
									"<head>"								   +
										"<base href='/banana'>"				   +
										"<script src='scrpt.js'></script>"	   +
									"</head>"								   +
									"<body>"								   +
									"<form action='frm'>"					   +
										"<a href='http://example.ca'>link</a>" +
									"</form>"								   +
									"</body>"								   +
								"</html>";
		InputSource src = new InputSource();
		src.setByteStream(new ByteArrayInputStream(document.getBytes()));
		src.setSystemId(TestConst.GOOD_URI_STR);
		
		LinkFinder finder = new LinkFinder();
		final ArrayList<String> foundLinks = new ArrayList<String>();
	
		finder.addLinkEventListener(new LinkEventHandler() {
			@Override
			public void linkFound(Location location, URI link) {
				foundLinks.add(link.toString());
				// TODO Test the actual locations to see if they are correct
			}
		});
		
		Parser parser = new Parser();
		parser.setContentHandler(finder);
		parser.parse(src);
		
		String[] expected = new String[] {
				TestConst.GOOD_URI_STR + "/banana/scrpt.js",
				TestConst.GOOD_URI_STR + "/banana/frm",
				"http://example.ca"
		};
		
		assertArrayEquals(expected, foundLinks.toArray());
	}

}
